# Minimal CMake configuration for Valhalla
#
# Builds libvalhalla and minimal collection of programs.
#
# This is NOT equivalent to the official Valhalla build configuration based on GNU Autotools.
# This is NOT suitable for building complete Valhalla suite.
# This is secondary build configuration provided for convenient development
# on Windows and using CMake-enabled IDEs.
#
cmake_minimum_required(VERSION 3.12 FATAL_ERROR)
project(valhalla LANGUAGES CXX C)

include(FindPkgConfig)
include(GNUInstallDirs)

set(VALHALLA_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
set(VALHALLA_BUILD_DIR ${CMAKE_CURRENT_BINARY_DIR})
list(INSERT CMAKE_MODULE_PATH 0 ${VALHALLA_SOURCE_DIR}/cmake)

set(CMAKE_CXX_STANDARD 17 CACHE STRING "C++ language version to use (default is 17)")
option(ENABLE_TOOLS "Enable Valhalla tools" ON)
option(ENABLE_DATA_TOOLS "Enable Valhalla data tools" ON)
option(ENABLE_SERVICES "Enable Valhalla services" ON)
option(ENABLE_HTTP "Enable the use of CURL" ON)
option(ENABLE_PYTHON_BINDINGS "Enable Python bindings" ON)
option(ENABLE_CCACHE "Speed up incremental rebuilds via ccache" ON)
option(ENABLE_COVERAGE "Build with coverage instrumentalisation" OFF)
option(ENABLE_COMPILER_WARNINGS "Build with compiler warnings" OFF)
option(ENABLE_SANITIZERS "Use all the integrated sanitizers for Debug build" OFF)
option(ENABLE_ADDRESS_SANITIZER "Use memory sanitizer for Debug build" OFF)
option(ENABLE_UNDEFINED_SANITIZER "Use UB sanitizer for Debug build" OFF)
option(ENABLE_TESTS "Enable Valhalla tests" ON)
option(ENABLE_WERROR "Convert compiler warnings to errors. Requires ENABLE_COMPILER_WARNINGS=ON to take effect" OFF)
option(ENABLE_BENCHMARKS "Enable microbenchmarking" ON)
option(ENABLE_THREAD_SAFE_TILE_REF_COUNT "If ON uses shared_ptr as tile reference(i.e. it is thread safe)" OFF)
option(ENABLE_SINGLE_FILES_WERROR "Convert compiler warnings to errors for single files" ON)
# useful to workaround issues likes this https://stackoverflow.com/questions/24078873/cmake-generated-xcode-project-wont-compile
option(ENABLE_STATIC_LIBRARY_MODULES "If ON builds Valhalla modules as STATIC library targets" OFF)

set(LOGGING_LEVEL "" CACHE STRING "Logging level, default is INFO")
set_property(CACHE LOGGING_LEVEL PROPERTY STRINGS "NONE;ALL;ERROR;WARN;INFO;DEBUG;TRACE")
set_property(GLOBAL PROPERTY USE_FOLDERS ON)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(THREADS_PREFER_PTHREAD_FLAG ON)

# colorize output
include(CheckCXXCompilerFlag)
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcolor-diagnostics")
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
  set(COLOR_FLAG "-fdiagnostics-color=auto")
  check_cxx_compiler_flag("-fdiagnostics-color=auto" HAS_COLOR_FLAG)
  if(NOT HAS_COLOR_FLAG)
    set(COLOR_FLAG "")
  endif()
  # using GCC
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COLOR_FLAG}")
endif()

# Explicitly set the build type to Release if no other type is specified
# on the command line.  Without this, cmake defaults to an unoptimized,
# non-debug build, which almost nobody wants.
if(NOT MSVC_IDE) # TODO: May need to be extended for Xcode, CLion, etc.
  if(NOT CMAKE_BUILD_TYPE)
    message(STATUS "No build type specified, defaulting to Release")
    set(CMAKE_BUILD_TYPE Release)
  endif()
endif()

if(CMAKE_BUILD_TYPE STREQUAL Debug)
  message(STATUS "Configuring in debug mode")
elseif(CMAKE_BUILD_TYPE STREQUAL Release)
  message(STATUS "Configuring in release mode")
elseif(CMAKE_BUILD_TYPE STREQUAL RelWithDebInfo)
  message(STATUS "Configuring in release mode with debug symbols")
elseif(CMAKE_BUILD_TYPE STREQUAL MinRelSize)
  message(STATUS "Configuring in release mode with minimized size")
elseif(CMAKE_BUILD_TYPE STREQUAL None)
  message(STATUS "Configuring without a mode, no optimization flags will be set")
else()
  message(FATAL_ERROR "Unrecognized build type. Use one of Debug, Release, RelWithDebInfo, MinRelSize, None")
endif()

function(create_source_groups prefix)
  foreach(file ${ARGN})
    get_filename_component(file "${file}" ABSOLUTE)
    string(FIND "${file}" "${PROJECT_BINARY_DIR}/" pos)
    if(pos EQUAL 0)
      source_group(TREE "${PROJECT_BINARY_DIR}/" PREFIX "Generated Files" FILES "${file}")
    else()
      source_group(TREE "${PROJECT_SOURCE_DIR}/" PREFIX "${prefix}" FILES "${file}")
    endif()
  endforeach()
endfunction()

if(ENABLE_CCACHE AND (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU"))
  find_program(CCACHE_FOUND ccache)
  if(CCACHE_FOUND)
    message(STATUS "Using ccache to speed up incremental builds")
    set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
    set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache)
    set(ENV{CCACHE_CPP2} "true")
  endif()
endif()

include(cmake/SanitizerOptions.cmake)

## Coverage report targets
if(ENABLE_COVERAGE)
  find_program(GENHTML_PATH NAMES genhtml genhtml.perl genhtml.bat)
  if(NOT GENHTML_PATH)
    message(FATAL_ERROR "no genhtml installed")
  endif()

  set(FASTCOV_PATH ${VALHALLA_SOURCE_DIR}/third_party/fastcov/fastcov.py)
  add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/coverage.info
    COMMAND ${FASTCOV_PATH} -d . --exclude /usr/ third_party/ ${CMAKE_CURRENT_BINARY_DIR}/ --lcov -o coverage.info
    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
    DEPENDS check)

  add_custom_target(coverage
    COMMAND ${GENHTML_PATH} --prefix ${CMAKE_CURRENT_BINARY_DIR} --output-directory coverage --title "Test Coverage" --legend --show-details coverage.info
    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
    DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/coverage.info)
  set_target_properties(coverage PROPERTIES FOLDER "Tests")
endif()

## Dependencies
find_package(Threads REQUIRED)
find_package(ZLIB REQUIRED)

# try to find an installed boost or install locally with conan
set(boost_VERSION "1.71")
find_package(Boost ${boost_VERSION} QUIET)
if (NOT Boost_FOUND)
  # bail if there's no conan installed
  message(STATUS "No compatible boost version detected, using conan...")
  find_program(conan_FOUND conan)
  if (NOT conan_FOUND)
    message(FATAL_ERROR "conan needs to be installed for boost, see https://docs.conan.io/en/latest/installation.html.")
  endif()

  # build dir needs to be added, that's where conan writes some info
  list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_BINARY_DIR})
  list(APPEND CMAKE_PREFIX_PATH ${CMAKE_CURRENT_BINARY_DIR})
  include(${VALHALLA_SOURCE_DIR}/cmake/conan.cmake)

  conan_cmake_autodetect(settings)
  conan_cmake_install(PATH_OR_REFERENCE ${VALHALLA_SOURCE_DIR}/conanfile.txt
                      REMOTE conancenter
                      SETTINGS ${settings}
                      OUTPUT_QUIET)
endif()
find_package(Boost ${boost_VERSION} REQUIRED)
add_definitions(-DBOOST_NO_CXX11_SCOPED_ENUMS)
add_definitions(-DBOOST_ALLOW_DEPRECATED_HEADERS)
add_definitions(-DBOOST_BIND_GLOBAL_PLACEHOLDERS)

if(NOT TARGET CURL::CURL)
  add_library(CURL::CURL INTERFACE IMPORTED)
  if(ENABLE_HTTP OR ENABLE_DATA_TOOLS)
    if(NOT CURL_FOUND)
      find_package(CURL REQUIRED)
    endif()
    set_target_properties(CURL::CURL PROPERTIES
      INTERFACE_INCLUDE_DIRECTORIES "${CURL_INCLUDE_DIR}"
      INTERFACE_COMPILE_DEFINITIONS CURL_STATICLIB)
    if(NOT MSVC)
      set_property(TARGET CURL::CURL APPEND PROPERTY INTERFACE_LINK_LIBRARIES "${CURL_LIBRARIES}")
    else()
      link_libraries(${CURL_LIBRARIES})
    endif()
  endif()
else()
  message(STATUS "Using curl from the outside")
endif()

if(NOT Protobuf_FOUND)
  find_package(Protobuf REQUIRED)

  # orchestrated effort of both protobuf & cmake are responsible for the next lines..

  # TODO: monitor how protobuf Linux packages evolve. Eventually everyone should use
  # the Apple configuration, but e.g. apt does not install CMake config with protobuf
  if(Protobuf_VERSION VERSION_GREATER_EQUAL 4.22.0)
    # this might be needed for some Apple/Linux protobuf versions
    # add_definitions(-DPROTOBUF_USE_DLLS)
    if(APPLE OR WIN32)
      # CONFIG mode is ideal for new abseil protobuf builds, which uses the protobuf-provided
      # CMake configuration instead of CMake internal one, as that's not looking for abseil:
      # https://github.com/valhalla/valhalla/issues/4173#issuecomment-1638067950
      # Newer protobuf versions require a protobuf_MODULE_COMPATIBLE to be able 
      # to see the protobuf_generate_cpp() CMake function
      set(protobuf_MODULE_COMPATIBLE ON CACHE BOOL "")
      find_package(Protobuf REQUIRED CONFIG)
    else()
      # linux might not have CMake config installed (e.g. Debian derivatives)
      # need to link protobuf to abseil manually
      find_package(Protobuf REQUIRED)
      find_package(absl REQUIRED)
      if(TARGET protobuf::libprotobuf-lite)
        target_link_libraries(protobuf::libprotobuf-lite INTERFACE "absl::absl_log")
      elseif(TARGET protobuf::libprotobuf)
        target_link_libraries(protobuf::libprotobuf INTERFACE "absl::absl_log")
      endif()
    endif()
  endif()
endif()

message(STATUS "Using pbf headers from ${PROTOBUF_INCLUDE_DIR}")
message(STATUS "Using pbf libs from ${PROTOBUF_LIBRARY}")
message(STATUS "Using pbf release libs from ${PROTOBUF_LIBRARY_RELEASE}")
message(STATUS "Using pbf debug libs from ${PROTOBUF_LIBRARY_DEBUG}")
if(TARGET protobuf::libprotobuf-lite)
  message(STATUS "Using pbf-lite")
endif()

# Allow linking against full or lite version of Protobuf library
# TODO: After switching to CMake 3.12+, replace with
#       $<TARGET_EXISTS:protobuf::libprotobuf
#       $<TARGET_EXISTS:protobuf::libprotobuf-lite
if(TARGET protobuf::libprotobuf-lite)
  set(valhalla_protobuf_targets protobuf::libprotobuf-lite)
elseif(TARGET protobuf::libprotobuf)
  set(valhalla_protobuf_targets protobuf::libprotobuf)
else()
  message(FATAL_ERROR "Required target protobuf::libprotobuf-lite or protobuf::libprotobuf is not defined")
endif()

add_library(libprime_server INTERFACE IMPORTED)
if(ENABLE_SERVICES)
  pkg_check_modules(libprime_server REQUIRED libprime_server>=0.6.3)
  # workaround for https://gitlab.kitware.com/cmake/cmake/issues/15804
  find_library(libprime_server_LIBRARY
    NAME ${libprime_server_LIBRARIES}
    HINTS ${libprime_server_LIBRARY_DIRS})
  set_target_properties(libprime_server PROPERTIES
    INTERFACE_LINK_LIBRARIES "${libprime_server_LIBRARY}"
    INTERFACE_INCLUDE_DIRECTORIES "${libprime_server_INCLUDE_DIRS}"
    INTERFACE_COMPILE_DEFINITIONS HAVE_HTTP)
endif()

## Mjolnir and associated executables
if(ENABLE_DATA_TOOLS)
  add_compile_definitions(DATA_TOOLS)
  find_package(SQLite3 REQUIRED)
  find_package(SpatiaLite REQUIRED)
  find_package(LuaJIT)
  add_library(Lua::Lua INTERFACE IMPORTED)
  set_target_properties(Lua::Lua PROPERTIES
    INTERFACE_LINK_LIBRARIES "${LUA_LIBRARIES}"
    INTERFACE_INCLUDE_DIRECTORIES "${LUA_INCLUDE_DIR}")
endif()

if (ENABLE_THREAD_SAFE_TILE_REF_COUNT)
 add_definitions(-DENABLE_THREAD_SAFE_TILE_REF_COUNT)
endif ()

## libvalhalla
add_subdirectory(src)

## Python bindings
if(ENABLE_PYTHON_BINDINGS)
  # favors Py3, falls back to Py2
  find_package(Python COMPONENTS Development Interpreter)
  if (Python_FOUND)
    add_subdirectory(src/bindings/python)
    install(FILES COPYING CHANGELOG.md
      DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/doc/python-valhalla"
      COMPONENT python)
  else()
    set(ENABLE_PYTHON_BINDINGS OFF)
    message(WARNING "Skipping Python bindings... Install a development Python version including headers to compile the bindings.")
  endif()
endif()

## Executable targets
function(get_source_path PATH NAME)
  if(EXISTS ${VALHALLA_SOURCE_DIR}/src/${NAME}.cc)
    set(${PATH} ${VALHALLA_SOURCE_DIR}/src/${NAME}.cc PARENT_SCOPE)
  elseif(EXISTS ${VALHALLA_SOURCE_DIR}/src/meili/${NAME}.cc)
    set(${PATH} ${VALHALLA_SOURCE_DIR}/src/meili/${NAME}.cc PARENT_SCOPE)
  elseif(EXISTS ${VALHALLA_SOURCE_DIR}/src/mjolnir/${NAME}.cc)
    set(${PATH} ${VALHALLA_SOURCE_DIR}/src/mjolnir/${NAME}.cc PARENT_SCOPE)
  else()
    message(FATAL_ERROR "no source path for ${NAME}")
  endif()
endfunction()

## Valhalla programs
set(valhalla_programs valhalla_run_map_match valhalla_benchmark_loki valhalla_benchmark_skadi
  valhalla_run_isochrone valhalla_run_route valhalla_benchmark_adjacency_list valhalla_run_matrix
  valhalla_path_comparison valhalla_export_edges valhalla_expand_bounding_box valhalla_service)

## Valhalla data tools
set(valhalla_data_tools valhalla_build_statistics valhalla_ways_to_edges valhalla_validate_transit
  valhalla_benchmark_admins valhalla_build_connectivity	valhalla_build_tiles valhalla_build_admins
  valhalla_convert_transit valhalla_ingest_transit valhalla_query_transit valhalla_add_predicted_traffic
  valhalla_assign_speeds valhalla_add_elevation valhalla_build_landmarks valhalla_add_landmarks)

## Valhalla services
set(valhalla_services valhalla_loki_worker valhalla_odin_worker valhalla_thor_worker)

if(ENABLE_TOOLS)
  foreach(program ${valhalla_programs})
    get_source_path(path ${program})
    add_executable(${program} ${path})
    set_target_properties(${program} PROPERTIES FOLDER "Tools")
    create_source_groups("Source Files" ${path})
    target_link_libraries(${program} valhalla)
    target_include_directories(${program} PUBLIC ${VALHALLA_SOURCE_DIR}/third_party/cxxopts/include)
    if(MSVC)
      target_link_libraries(${program} ${CURL_LIBRARIES})
    endif()
    install(TARGETS ${program} DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime)
  endforeach()
endif()

if(ENABLE_DATA_TOOLS)
  foreach(program ${valhalla_data_tools})
    get_source_path(path ${program})
    add_executable(${program} ${path})
    create_source_groups("Source Files" ${path})
    set_target_properties(${program} PROPERTIES FOLDER "Data Tools")
    target_include_directories(${program} PUBLIC ${VALHALLA_SOURCE_DIR}/third_party/cxxopts/include)
    target_link_libraries(${program} valhalla)
    if (LUAJIT_FOUND AND APPLE AND CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "x86_64")
      # Using LuaJIT on macOS on Intel processors requires a couple of extra linker flags
      target_link_options(${program} PUBLIC -pagezero_size 10000 -image_base 100000000)
    endif()
    if(MSVC)
      target_link_libraries(${program} ${CURL_LIBRARIES})
    endif()
    install(TARGETS ${program} DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime)
  endforeach()

  # Target-specific dependencies
  find_package(GEOS)
  target_link_libraries(valhalla_build_admins GEOS::GEOS)
  target_sources(valhalla_build_statistics
    PUBLIC
      ${VALHALLA_SOURCE_DIR}/src/mjolnir/statistics.cc
      ${VALHALLA_SOURCE_DIR}/src/mjolnir/statistics_database.cc)
endif()

if(ENABLE_SERVICES)
  foreach(program ${valhalla_services})
    add_executable(${program} src/${program}.cc)
    create_source_groups("Source Files" src/${program}.cc)
    set_target_properties(${program} PROPERTIES FOLDER "Services")
    target_link_libraries(${program} valhalla)
    target_include_directories(${program} PUBLIC ${VALHALLA_SOURCE_DIR}/third_party/cxxopts/include)
    install(TARGETS ${program} DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT runtime)
  endforeach()
endif()

# add the scripts to the build folder as well
foreach(script valhalla_build_config valhalla_build_elevation
  valhalla_build_extract valhalla_build_timezones)
  configure_file(${VALHALLA_SOURCE_DIR}/scripts/${script} ${CMAKE_BINARY_DIR}/${script} COPYONLY)
  
  install(
    FILES
      scripts/${script}
    DESTINATION "${CMAKE_INSTALL_BINDIR}"
    PERMISSIONS
      OWNER_READ OWNER_WRITE OWNER_EXECUTE
      GROUP_READ GROUP_EXECUTE
      WORLD_READ WORLD_EXECUTE
      COMPONENT runtime
    )
endforeach()

install(FILES COPYING CHANGELOG.md
  DESTINATION "${CMAKE_INSTALL_DOCDIR}"
  COMPONENT runtime)

if(ENABLE_TESTS)
  add_subdirectory(test)
endif()

# NOTE(mookerji): Windows CI seems to break on the gbench build, so shelve Win32 support for now.
if(ENABLE_BENCHMARKS AND ENABLE_DATA_TOOLS AND NOT WIN32)
  add_subdirectory(bench)
endif()

## Packaging via CPack
include(CPackComponent)

string(TOLOWER "${CMAKE_PROJECT_NAME}" CPACK_PACKAGE_NAME)
set(CPACK_PACKAGE_VERSION_MAJOR ${VALHALLA_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${VALHALLA_VERSION_MINOR})
set(CPACK_PACKAGE_VERSION_PATCH ${VALHALLA_VERSION_PATCH})
set(CPACK_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}${CPACK_PACKAGE_VERSION_SUFFIX}")
set(CPACK_PACKAGE_CONTACT "Team Valhalla <valhalla@mapzen.com>")
set(CPACK_RESOURCE_FILE_LICENSE "${VALHALLA_SOURCE_DIR}/LICENSE.md")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "OpenStreetMap Routing API
 A set of routing APIs designed around OSM map data using
 dynamic costing and a tiled data structure")
  set(CPACK_COMPONENT_PYTHON_DESCRIPTION "OpenStreetMap Routing Python Bindings
 A set routing APIs designed around OSM map data using
 dynamic costing and a tiled data structure and
 accompanying tools and services used to analyse and
 compute routes using those APIs")
set(CPACK_STRIP_FILES TRUE)
set(CPACK_SOURCE_PACKAGE_FILE_NAME "libvalhalla")

if(${CPACK_GENERATOR} MATCHES "^DEB$")
  set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/valhalla/")
  set(CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT")
  set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)

  set(CPACK_DEBIAN_SHARED_PACKAGE_NAME "libvalhalla0")
  set(CPACK_DEBIAN_SHARED_PACKAGE_SECTION "contrib/libs")

  set(CPACK_DEBIAN_DEVELOPMENT_PACKAGE_NAME "libvalhalla-dev")
  set(CPACK_DEBIAN_DEVELOPMENT_PACKAGE_DEPENDS "libvalhalla0 (= ${CPACK_PACKAGE_VERSION})")

  set(CPACK_DEBIAN_RUNTIME_PACKAGE_NAME "valhalla-bin")
  set(CPACK_DEBIAN_RUNTIME_PACKAGE_SECTION "contrib/misc")
  set(CPACK_DEBIAN_RUNTIME_PACKAGE_DEPENDS "libvalhalla0 (= ${CPACK_PACKAGE_VERSION})")

  set(CPACK_DEBIAN_PYTHON_PACKAGE_NAME "python-valhalla")
  set(CPACK_DEBIAN_PYTHON_PACKAGE_SECTION "python")
  set(CPACK_DEBIAN_PYTHON_PACKAGE_DEPENDS "libvalhalla0 (= ${CPACK_PACKAGE_VERSION})")

  if("${CMAKE_CXX_LIBRARY_ARCHITECTURE}" MATCHES "arm-linux-gnueabihf")
    set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE armhf)
  endif()

  message(STATUS "Configured deb packages of ${CMAKE_CXX_LIBRARY_ARCHITECTURE} build for ${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}")
endif()

if(BUILD_SHARED_LIBS)
  set(CPACK_COMPONENTS_ALL "shared;runtime;python")
else()
  set(CPACK_COMPONENTS_ALL "development")
endif()

set(CPACK_PROJECT_CONFIG_FILE ${VALHALLA_SOURCE_DIR}/cmake/CPackConfig.cmake)
set(CPACK_DEBIAN_PACKAGE_DEBUG OFF)
include(CPack)
